16.4 回收

内存回收的源头是垃圾清理操作。

之所以说回收而非释放,是因为整个内存分配器的核心是内存复用,不再使用的内存会被放回合适位置,等下次分配时再次使用。只有当空闲内存资源过多时,才会考虑释放。

基于效率考虑,回收操作自然不会直接盯着单个对象,而是以span为基本单位。通过比对bitmap里的扫描标记,逐步将object收归原span,最终上交central或heap复用。

清理函数sweepone调用mSpan_Sweep来引发内存分配器回收流程。

mgcsweep.go

func sweepone()uintptr{ if!mSpan_Sweep(s,false) { … } }

func mSpan_Sweep(s*mspan,preserve bool)bool{ var head,end gclinkptr

// 为span中的空闲object设置标记,无须再次扫描 for link:=s.freelist;link.ptr() !=nil;link=link.ptr().next{ heapBitsForAddr(uintptr(link)).setMarkedNonAtomic() }

// 遍历span,收集未标记的不可达object(不包括freelist,它们已被标记) heapBitsSweepSpan(s.base(),size,n,func(p uintptr) { if cl==0{ // 大对象:重置bitmap,更新sweepgen heapBitsForSpan(p).initSpan(s.layout()) atomicstore(&s.sweepgen,sweepgen) freeToHeap=true }else{ // 使用head、end构建链表,收集不可达object if head.ptr() ==nil{ head=gclinkptr(p) }else{ end.ptr().next=gclinkptr(p) } end=gclinkptr(p) end.ptr().next=gclinkptr(0x0bade5)

     // 收集计数 
    nfree++ 
 } 

})

// 回收内存 // 小对象:如果没有可回收object,那么维持原状态,根本无须处理 // 大对象:整个span就是一个object,直接交还heap if nfree>0{ mCentral_FreeSpan(&mheap_.central[cl].mcentral,s,int32(nfree),head,end, …) }else if freeToHeap{ mHeap_Free(&mheap_,s,1) } }

遍历span,将收集到的不可达object合并到freelist链表。如该span已收回全部object,那么就将这块完全自由的内存还给heap,以便后续复用。

mcentral.go

func mCentral_FreeSpan(cmcentral,smspan,n int32,start,end gclinkptr…)bool{ // 判断span是否为空(没有空闲object) wasempty:=s.freelist.ptr() ==nil

// 将收集到链表合并到freelist end.ptr().next=s.freelist s.freelist=start s.ref-=uint16(n)

// 阻止进一步回收 if preserve{ atomicstore(&s.sweepgen,mheap_.sweepgen) return false }

// 将原本为空的span转移到central.nonempty链表 if wasempty{ mSpanList_Remove(s) mSpanList_Insert(&c.nonempty,s) }

// 如果还有object被使用,那么终止 if s.ref!=0{ return false }

// 如果收回全部object,就从central交还给heap mSpanList_Remove(s) heapBitsForSpan(s.base()).initSpan(s.layout()) mHeap_Free(&mheap_,s,0)

return true }

无论是向操作系统申请内存,还是清理回收内存,只要往heap里放span,都会尝试合并左右相邻的闲置span,以构成更大的自由块。

mheap.go

func mHeap_Free(hmheap,smspan,acct int32) { systemstack(func() { mHeap_FreeSpanLocked(h,s,true,true,0) }) }

func mHeap_FreeSpanLocked(hmheap,smspan,acctinuse,acctidle bool,unusedsince int64) { // 从现有链表移除 mSpanList_Remove(s)

// 计算偏移量 p:=uintptr(s.start) p-=uintptr(unsafe.Pointer(h.arena_start)) >> _PageShift

if p>0{ // 通过spans数组访问左侧相邻span t:=h_spans[p-1]

 // 检查合并条件 
if t!=nil&&t.state!= _MSpanInUse&&t.state!= _MSpanStack{ 
     // 合并,更新属性 
    s.start=t.start
    s.npages+=t.npages

     // 更新spans里的信息 
    p-=t.npages
    h_spans[p] =s

     // 释放原左侧span对象 
    mSpanList_Remove(t) 
    fixAlloc_Free(&h.spanalloc, (unsafe.Pointer)(t)) 
 } 

}

// 检查右侧span if(p+s.npages)*ptrSize<h.spans_mapped{ t:=h_spans[p+s.npages] if t!=nil&&t.state!= _MSpanInUse&&t.state!= _MSpanStack{ // 合并右侧span,更新属性 s.npages+=t.npages

     // 更新spans信息 
    h_spans[p+s.npages-1] =s

     // 释放原右侧span对象 
    mSpanList_Remove(t) 
    fixAlloc_Free(&h.spanalloc, (unsafe.Pointer)(t)) 
 } 

}

// 根据页数插入free/freelarge链表 if s.npages<uintptr(len(h.free)) { mSpanList_Insert(&h.free[s.npages],s) }else{ mSpanList_Insert(&h.freelarge,s) } }

回收操作至此结束。这些被收回的span并不会被释放,而是等待复用。